#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
重复文件查找与清理工具

功能：
1. 扫描指定目录下的所有文件
2. 通过文件哈希值识别重复文件
3. 以图形界面展示重复文件组
4. 允许用户勾选并删除不需要的重复文件

使用说明：
1. 点击"选择目录"按钮选择要扫描的文件夹
2. 等待扫描完成后，查看重复文件列表
3. 勾选要删除的文件
4. 点击"删除选中文件"按钮执行删除操作
"""

import os
import hashlib
import tkinter as tk
from tkinter import filedialog, ttk, messagebox
from collections import defaultdict
import threading
import time

# 统一路径分隔符为'/'
def normalize_path(path):
    return path.replace('\\', '/')


class DuplicateFileCleaner:
    def __init__(self, root):
        """初始化应用程序"""
        self.root = root
        self.root.title("重复文件查找与清理工具")
        self.root.geometry("1200x600")  # 增加宽度以容纳所有UI组件
        self.root.resizable(True, True)

        # 设置中文字体支持
        self.style = ttk.Style()
        self.style.configure("Treeview.Heading", font=("SimHei", 10, "bold"))
        self.style.configure("Treeview", font=("SimHei", 10))

        # 存储扫描结果和文件夹信息
        self.duplicate_files = defaultdict(list)
        self.duplicate_folders = set()
        self.selected_directory = None
        self.scanning = False

        # 创建UI
        self.create_widgets()

    def create_widgets(self):
        """创建用户界面组件"""
        # 顶部按钮区域
        top_frame = ttk.Frame(self.root, padding="10")
        top_frame.pack(fill=tk.X, expand=False)

        # 选择目录按钮
        self.select_dir_btn = ttk.Button(
            top_frame, text="选择目录", command=self.select_directory
        )
        self.select_dir_btn.pack(side=tk.LEFT, padx=5)

        # 扫描按钮
        self.scan_btn = ttk.Button(
            top_frame, text="开始扫描", command=self.start_scan
        )
        self.scan_btn.pack(side=tk.LEFT, padx=5)
        self.scan_btn.config(state=tk.DISABLED)

        # 文件大小过滤区域
        size_frame = ttk.Frame(top_frame, padding="5")
        size_frame.pack(side=tk.LEFT, padx=10)

        ttk.Label(size_frame, text="最小文件大小: ").pack(side=tk.LEFT)

        self.size_var = tk.StringVar(value="0")
        self.size_entry = ttk.Entry(size_frame, textvariable=self.size_var, width=10)
        self.size_entry.pack(side=tk.LEFT, padx=5)

        self.size_unit_var = tk.StringVar(value="KB")
        self.size_unit_combobox = ttk.Combobox(
            size_frame, textvariable=self.size_unit_var, state="readonly", width=5
        )
        self.size_unit_combobox['values'] = ('B', 'KB', 'MB', 'GB')
        self.size_unit_combobox.pack(side=tk.LEFT, padx=5)

        # 文件夹操作区域
        folder_frame = ttk.Frame(top_frame, padding="5")
        folder_frame.pack(side=tk.LEFT, padx=10)

        ttk.Label(folder_frame, text="文件夹: ").pack(side=tk.LEFT)

        self.folder_var = tk.StringVar()
        self.folder_combobox = ttk.Combobox(
            folder_frame, textvariable=self.folder_var, state="normal", width=30
        )
        self.folder_combobox.pack(side=tk.LEFT, padx=5)

        # 保留和删除按钮
        self.keep_folder_btn = ttk.Button(
            folder_frame, text="保留文件夹文件", command=lambda: self.folder_operation('keep')
        )
        self.keep_folder_btn.pack(side=tk.LEFT, padx=5)
        self.keep_folder_btn.config(state=tk.DISABLED)

        self.delete_folder_btn = ttk.Button(
            folder_frame, text="删除文件夹文件", command=lambda: self.folder_operation('delete')
        )
        self.delete_folder_btn.pack(side=tk.LEFT, padx=5)
        self.delete_folder_btn.config(state=tk.DISABLED)

        # 删除按钮
        self.delete_btn = ttk.Button(
            top_frame, text="删除选中文件", command=self.delete_selected_files
        )
        self.delete_btn.pack(side=tk.LEFT, padx=5)
        self.delete_btn.config(state=tk.DISABLED)

        # 状态标签
        self.status_var = tk.StringVar()
        self.status_var.set("请选择要扫描的目录")
        self.status_label = ttk.Label(
            top_frame, textvariable=self.status_var, foreground="blue"
        )
        self.status_label.pack(side=tk.RIGHT, padx=5)

        # 进度条
        self.progress_var = tk.DoubleVar()
        self.progress_bar = ttk.Progressbar(
            top_frame, variable=self.progress_var, length=200
        )
        self.progress_bar.pack(side=tk.RIGHT, padx=5)

        # 中间分隔符
        ttk.Separator(self.root).pack(fill=tk.X, pady=5)

        # 重复文件列表区域
        list_frame = ttk.Frame(self.root, padding="10")
        list_frame.pack(fill=tk.BOTH, expand=True)

        # 创建树状视图
        columns = ("select", "file_path", "size", "modified")
        self.tree = ttk.Treeview(
            list_frame, columns=columns, show="headings", selectmode="extended"
        )

        # 设置列标题
        self.tree.heading("select", text="选择")
        self.tree.heading("file_path", text="文件路径")
        self.tree.heading("size", text="文件大小 (KB)")
        self.tree.heading("modified", text="修改时间")

        # 设置列宽
        self.tree.column("select", width=50, anchor=tk.CENTER)
        self.tree.column("file_path", width=500, anchor=tk.W)
        self.tree.column("size", width=100, anchor=tk.E)
        self.tree.column("modified", width=150, anchor=tk.CENTER)

        # 滚动条
        scrollbar_y = ttk.Scrollbar(
            list_frame, orient=tk.VERTICAL, command=self.tree.yview
        )
        scrollbar_x = ttk.Scrollbar(
            list_frame, orient=tk.HORIZONTAL, command=self.tree.xview
        )
        self.tree.configure(
            yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set
        )

        # 布局
        scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y)
        scrollbar_x.pack(side=tk.BOTTOM, fill=tk.X)
        self.tree.pack(fill=tk.BOTH, expand=True)

        # 底部说明区域
        bottom_frame = ttk.Frame(self.root, padding="10")
        bottom_frame.pack(fill=tk.X, expand=False)

        self.info_label = ttk.Label(
            bottom_frame, 
            text="说明：勾选要删除的文件，然后点击'删除选中文件'按钮。相同哈希值的文件会被分组显示。",
            foreground="gray"
        )
        self.info_label.pack(anchor=tk.W)

    def select_directory(self):
        """选择要扫描的目录"""
        directory = filedialog.askdirectory(title="选择要扫描的目录")
        if directory:
            self.selected_directory = directory
            self.status_var.set(f"已选择目录：{normalize_path(directory)}")
            self.scan_btn.config(state=tk.NORMAL)
            # 清空之前的扫描结果
            self.clear_treeview()
            self.duplicate_files.clear()
            self.delete_btn.config(state=tk.DISABLED)

    def start_scan(self):
        """开始扫描重复文件"""
        if not self.selected_directory:
            messagebox.showwarning("警告", "请先选择要扫描的目录")
            return

        # 禁用按钮
        self.select_dir_btn.config(state=tk.DISABLED)
        self.scan_btn.config(state=tk.DISABLED)
        self.delete_btn.config(state=tk.DISABLED)

        # 清空之前的扫描结果
        self.clear_treeview()
        self.duplicate_files.clear()

        # 更新状态
        self.status_var.set("正在扫描文件...")
        self.scanning = True
        self.progress_var.set(0)

        # 在新线程中执行扫描
        scan_thread = threading.Thread(target=self.scan_files)
        scan_thread.daemon = True
        scan_thread.start()

    def scan_files(self):
        """扫描文件并找出重复文件"""
        try:
            # 获取所有文件路径
            all_files = []
            for root_dir, _, filenames in os.walk(self.selected_directory):
                for filename in filenames:
                    file_path = os.path.join(root_dir, filename).replace('\\', '/')
                    all_files.append(file_path)

            total_files = len(all_files)
            if total_files == 0:
                self.root.after(0, lambda: self.status_var.set("未找到任何文件"))
                return

            # 计算文件哈希值
            file_hashes = defaultdict(list)
            for i, file_path in enumerate(all_files):
                if not self.scanning:  # 检查是否取消扫描
                    break

                try:
                    # 更新进度
                    progress = (i + 1) / total_files * 100
                    self.root.after(0, lambda p=progress: self.progress_var.set(p))
                    self.root.after(0, lambda i=i+1, t=total_files: self.status_var.set(f"正在扫描文件... ({i}/{t})")
                    )

                    # 计算文件哈希值（使用文件大小+修改时间作为初步筛选，提高性能）
                    file_stat = os.stat(file_path)
                    file_size = file_stat.st_size
                    modified_time = file_stat.st_mtime

                    # 检查文件大小是否大于等于指定阈值
                    try:
                        min_size = float(self.size_var.get())
                        unit = self.size_unit_var.get()

                        # 转换为字节
                        if unit == 'KB':
                            min_size_bytes = min_size * 1024
                        elif unit == 'MB':
                            min_size_bytes = min_size * 1024 * 1024
                        elif unit == 'GB':
                            min_size_bytes = min_size * 1024 * 1024 * 1024
                        else:  # B
                            min_size_bytes = min_size

                        # 如果文件大小小于阈值，跳过
                        if file_size < min_size_bytes:
                            continue
                    except ValueError:
                        # 如果输入无效，使用默认值0（不过滤）
                        pass

                    # 对于小文件，直接计算完整哈希值
                    if file_size < 1024 * 10:  # 10KB
                        file_hash = self.calculate_file_hash(file_path)
                        key = (file_size, file_hash)
                    else:
                        # 对于大文件，先使用部分内容计算哈希值
                        file_hash = self.calculate_partial_file_hash(file_path)
                        key = (file_size, file_hash)

                    file_hashes[key].append((file_path, file_size, modified_time))

                except Exception as e:
                    self.root.after(0, lambda fp=file_path, err=str(e): self.status_var.set(f"扫描文件出错: {normalize_path(fp)} - {err}"))
                    continue

            # 找出重复文件
            if self.scanning:
                self.duplicate_files = {k: v for k, v in file_hashes.items() if len(v) > 1}
                self.root.after(0, self.display_duplicate_files)

        except Exception as e:
            self.root.after(0, lambda err=str(e): messagebox.showerror("错误", f"扫描过程中出错: {err}"))
        finally:
            if self.scanning:
                self.root.after(0, self.scan_complete)

    def calculate_file_hash(self, file_path, block_size=65536):
        """计算整个文件的MD5哈希值"""
        hasher = hashlib.md5()
        with open(file_path, 'rb') as file:
            buf = file.read(block_size)
            while len(buf) > 0:
                hasher.update(buf)
                buf = file.read(block_size)
        return hasher.hexdigest()

    def calculate_partial_file_hash(self, file_path, block_size=65536, num_blocks=4):
        """计算文件部分内容的哈希值，用于大文件快速比较"""
        hasher = hashlib.md5()
        file_size = os.path.getsize(file_path)

        with open(file_path, 'rb') as file:
            # 读取开头的几个块
            for _ in range(num_blocks):
                buf = file.read(block_size)
                if not buf:
                    break
                hasher.update(buf)

            # 如果文件足够大，再读取中间和结尾的块
            if file_size > block_size * (num_blocks + 2):
                # 中间位置
                file.seek(file_size // 2)
                buf = file.read(block_size)
                hasher.update(buf)

                # 接近结尾的位置
                file.seek(max(0, file_size - block_size * num_blocks))
                for _ in range(num_blocks):
                    buf = file.read(block_size)
                    if not buf:
                        break
                    hasher.update(buf)

        return hasher.hexdigest()

    def display_duplicate_files(self):
        """在树状视图中显示重复文件，并自动勾选除一个外的所有重复文件"""
        # 清空树状视图
        self.clear_treeview()

        if not self.duplicate_files:
            self.status_var.set("未找到重复文件")
            return

        # 收集所有包含重复文件的文件夹
        self.duplicate_folders = set()
        for file_group in self.duplicate_files.values():
            for file_path, _, _ in file_group:
                folder = normalize_path(os.path.dirname(file_path))
                self.duplicate_folders.add(folder)

        # 更新文件夹选择框
        self.update_folder_combobox()

        # 为每个重复文件组添加分隔符
        group_count = 0
        for file_group in self.duplicate_files.values():
            group_count += 1
            # 添加组分隔符
            self.tree.insert(
                "", tk.END, values=("", f"========== 重复文件组 {group_count} ==========", "", ""), tags=("group",)
            )
            # 添加文件项
            for i, (file_path, file_size, modified_time) in enumerate(file_group):
                # 格式化修改时间
                modified_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(modified_time))
                # 格式化文件大小（KB）
                size_kb = round(file_size / 1024, 2)
                # 插入到树状视图
                item_id = self.tree.insert(
                    "", tk.END, values=("", file_path, size_kb, modified_str), tags=("file",)
                )
                # 自动勾选除第一个外的所有文件
                if i > 0:
                    self.tree.set(item_id, "select", "☑")
                else:
                    self.tree.set(item_id, "select", "□")

        # 设置标签样式
        self.tree.tag_configure("group", background="#e0e0e0", font=("SimHei", 10, "bold"))
        self.tree.tag_configure("file", background="white")

        # 绑定双击事件用于切换选择状态
        self.tree.bind("<Double-1>", self.toggle_select)

        self.status_var.set(f"找到 {len(self.duplicate_files)} 组重复文件")
        self.delete_btn.config(state=tk.NORMAL)

    def toggle_select(self, event):
        """切换文件的选择状态"""
        # 获取双击的项
        item = self.tree.identify_row(event.y)
        if not item:
            return

        # 检查是否是文件项
        tags = self.tree.item(item, "tags")
        if "file" not in tags:
            return

        # 切换选择状态
        current_value = self.tree.set(item, "select")
        new_value = "☑" if current_value == "□" else "□"
        self.tree.set(item, "select", new_value)

    def delete_selected_files(self):
        """删除选中的文件"""
        # 确认删除操作
        confirm = messagebox.askyesno("确认删除", "确定要删除选中的文件吗？此操作不可撤销！")
        if not confirm:
            return

        # 获取所有选中的文件
        selected_files = []
        for item in self.tree.get_children():
            tags = self.tree.item(item, "tags")
            if "file" in tags and self.tree.set(item, "select") == "☑":
                file_path = self.tree.set(item, "file_path")
                selected_files.append(file_path)

        if not selected_files:
            messagebox.showinfo("提示", "没有选中要删除的文件")
            return

        # 执行删除
        deleted_count = 0
        failed_count = 0
        failed_files = []

        for file_path in selected_files:
            try:
                os.remove(file_path)
                deleted_count += 1
                # 从树状视图中删除
                for item in self.tree.get_children():
                    if self.tree.set(item, "file_path") == file_path:
                        self.tree.delete(item)
                        break
            except Exception as e:
                failed_count += 1
                failed_files.append((file_path, str(e)))

        # 显示结果
        result_msg = f"删除完成：成功删除 {deleted_count} 个文件"
        if failed_count > 0:
            result_msg += f"，{failed_count} 个文件删除失败：\n"
            for file_path, error in failed_files:
                result_msg += f"- {file_path}: {error}\n"

        messagebox.showinfo("删除结果", result_msg)

        # 更新状态
        self.status_var.set(f"已删除 {deleted_count} 个文件")

    def clear_treeview(self):
        """清空树状视图"""
        for item in self.tree.get_children():
            self.tree.delete(item)

    def scan_complete(self):
        """扫描完成后启用按钮"""
        self.select_dir_btn.config(state=tk.NORMAL)
        self.scan_btn.config(state=tk.NORMAL)
        self.keep_folder_btn.config(state=tk.NORMAL if self.duplicate_folders else tk.DISABLED)
        self.delete_folder_btn.config(state=tk.NORMAL if self.duplicate_folders else tk.DISABLED)
        self.scanning = False

    def update_folder_combobox(self):
        """更新文件夹选择框"""
        self.folder_combobox['values'] = sorted(self.duplicate_folders)
        if self.duplicate_folders:
            self.folder_combobox.current(0)
            self.keep_folder_btn.config(state=tk.NORMAL)
            self.delete_folder_btn.config(state=tk.NORMAL)
        else:
            self.keep_folder_btn.config(state=tk.DISABLED)
            self.delete_folder_btn.config(state=tk.DISABLED)

    def folder_operation(self, operation):
        """根据选择执行保留或删除文件夹及其子文件夹中的重复文件"""
        selected_folder = self.folder_var.get()
        if not selected_folder:
            messagebox.showwarning("警告", "请先选择文件夹")
            return

        # 规范化文件夹路径（确保末尾有分隔符）
        selected_folder = os.path.normpath(selected_folder).replace('\\', '/')
        if not selected_folder.endswith(os.path.sep):
            selected_folder += os.path.sep

        # 转换为绝对路径，确保一致性
        try:
            selected_folder = normalize_path(os.path.abspath(selected_folder))
        except Exception as e:
            messagebox.showerror("错误", f"转换为绝对路径失败: {str(e)}")
            return

        # 统计匹配的文件数量
        matched_count = 0
        action_text = ""  # 初始化操作文本

        # 遍历所有文件项
        for item in self.tree.get_children():
            tags = self.tree.item(item, "tags")
            if "file" in tags:
                file_path = self.tree.set(item, "file_path")
                # 确保文件路径是字符串类型
                file_path = str(file_path)
                try:
                    # 获取文件所在文件夹的绝对路径
                    file_folder = normalize_path(os.path.abspath(os.path.dirname(file_path)))
                    # 添加分隔符（使用'/'）
                    file_folder += '/' if not file_folder.endswith('/') else ''

                    # 检查文件是否在所选文件夹或其子文件夹中
                    # 使用大小写不敏感的比较
                    # 确保比较时使用相同的分隔符
                    is_in_selected_folder = file_folder.lower().startswith(selected_folder.lower())

                    if is_in_selected_folder:
                        current_state = self.tree.set(item, "select")
                        if operation == 'keep':
                            # 保留文件 - 取消勾选
                            new_state = "□"
                            action_text = "取消勾选"
                        else:
                            # 删除文件 - 勾选
                            new_state = "☑"
                            action_text = "勾选"
                        self.tree.set(item, "select", new_state)
                        # 强制更新UI
                        self.tree.update_idletasks()
                        matched_count += 1
                except Exception as e:
                    messagebox.showerror("错误", f"处理文件失败 '{file_path}': {str(e)}")

        operation_text = "保留" if operation == 'keep' else "删除"
        messagebox.showinfo("提示", f"已设置{operation_text}文件夹 '{selected_folder}' 及其子文件夹中的文件，共 {matched_count} 个文件被{action_text}")


if __name__ == "__main__":
    """主函数"""
    root = tk.Tk()
    app = DuplicateFileCleaner(root)
    root.mainloop()